Discord Interaction Endpoint を多段 Lambda 構成にしてタイムアウトを回避する
前回の記事の続きです。
Discord の Interaction Endpoint は仕様上、Interaction リクエストから応答までに制限時間が設けられています。
制限時間を超えてしまう場合は、一旦仮の応答を返しておき、あとから HTTP リクエストで応答を修正する方法 (DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE) が用意されています。
API Gateway + Lambda で Interaction Endpoint を実装している場合は、多段 Lambda 構成にすることで実装することができます。
CDK スタックを用意する
import * as cdk from "aws-cdk-lib"; import { Duration } from "aws-cdk-lib"; import { FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda"; import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; import { Construct } from "constructs"; export class AppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const responseGeneratorHandler = new NodejsFunction( this, "generate-response", { entry: "./src/generate-response.ts", handler: "handler", environment: { DISCORD_APP_ID: "BotのアプリID" }, timeout: Duration.seconds(30) // Lambdaのタイムアウトを延長させておく } ); const initialHandler = new NodejsFunction(this, "initial-handler", { entry: "./src/initial-handler.ts", handler: "handler", environment: { DISCORD_PUBLIC_KEY: "BotのPublic Key", RESPONSE_GENERATOR_FUNCTION: responseGeneratorHandler.functionName, } }); initialHandler.addFunctionUrl({ authType: FunctionUrlAuthType.NONE, }); responseGeneratorHandler.grantInvoke(initialHandler); } }
多段 Lambda 構成なので、Webhook のハンドラー initial-handler
と、実際にレスポンスを返す処理 generate-response
を用意します。
initial-handler
は generate-response
を呼び出すので、grantInvoke で呼び出し権限を与えます。
Lambda 関数のソースコードを用意する
Webhook のハンドラ
initial-handler
は Interaction Request の検証処理と Lambda の起動処理を行います。
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; import { InteractionResponseType, InteractionType, verifyKey, } from "discord-interactions"; import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda"; // Interaction リクエストの署名検証 (ないと失敗する) const verifyRequest = (event: APIGatewayProxyEventV2) => { const { headers, body } = event; const signature = headers["x-signature-ed25519"]; const timestamp = headers["x-signature-timestamp"]; const publicKey = process.env["DISCORD_PUBLIC_KEY"]; if (!body || !signature || !timestamp || !publicKey) { return false; } return verifyKey(body, signature, timestamp, publicKey); }; // interaction の処理 const handleInteraction = async (body: Record<string, unknown>) => { if ( body.type === InteractionType.APPLICATION_COMMAND || body.type === InteractionType.MESSAGE_COMPONENT ) { // 別の Lambda に処理を委譲 const client = new LambdaClient({}); await client.send( new InvokeCommand({ FunctionName: process.env["RESPONSE_GENERATOR_FUNCTION"], InvocationType: "Event", Payload: Buffer.from( JSON.stringify({ body, }) ), }) ); // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE で先に応答しておく return { type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, }; } return { type: InteractionResponseType.PONG }; }; export const handler = async ( event: APIGatewayProxyEventV2 ): Promise<APIGatewayProxyResultV2> => { if (!verifyRequest(event)) { return { statusCode: 400, }; } const { body } = event; const interaction = JSON.parse(body!); return { statusCode: 200, headers: { "Content-Type": "application/json", }, body: JSON.stringify(await handleInteraction(interaction)), }; };
インタラクションを返す処理
メッセージオブジェクトを `https://discord.com/api/v9/webhooks/${applicationId}/${interactionToken}` に POST することで、遅延メッセージを確定させることができます。
import { APIGatewayProxyResultV2 } from "aws-lambda"; import axios from "axios"; import { InteractionType } from "discord-interactions"; export const handler = async (event: { body: Record<string, unknown>; }): Promise<APIGatewayProxyResultV2> => { const { body } = event; if (!body) { return { statusCode: 400 }; } const { token: interactionToken } = body; const followup = await new Promise((resolve, reject) => { // 5秒かかる処理 const resp = invokeCommand(body); setTimeout(() => { resolve(resp); }, 5000); }); try { const axiosResult = await axios.post( `https://discord.com/api/v9/webhooks/${process.env[ "DISCORD_APP_ID" ]!}/${interactionToken}`, JSON.stringify(followup), { headers: { "Content-Type": "application/json", }, } ); return { statusCode: axiosResult.status }; } catch (err) { console.error(err); return { statusCode: 500 }; } }; function invokeCommand(interaction: Record<string, unknown>) { if (interaction.type === InteractionType.APPLICATION_COMMAND) { const data = interaction.data as Record<string, unknown>; if (data.name === "hello") { return { content: "deferred message", }; } } }
やってみる
コマンドを実行するとプレースホルダーのメッセージが表示されます。
5秒後にメッセージが返ってきます。
まとめ
Interaction Endpoint のタイムアウトを延長することができるので、データベースへのアクセスなど遅くなる処理も書くことができるようになります。
今回はシンプルに多段 Lambda 構成で実装してみました。